/*
 * Copyright (C) 2012-2013  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.remote.topology;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import nemos.util.log.NemosLogger;
import nemos.util.osgi.FilterBuilder;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.remoteserviceadmin.ExportRegistration;
import org.osgi.service.remoteserviceadmin.RemoteServiceAdmin;
import org.osgi.util.tracker.ServiceTracker;

/**
 * ExportTopologyManager : The export-side of the topology manager.
 * 
 * <p>
 * This topology manager exports all eligible local services.
 * </p>
 * 
 * @author Romain DUBOIS
 */
@Component
public class ExportTopologyManager {

    /** Output : Remote service admins */
    @Reference(name = "rsa", referenceInterface = RemoteServiceAdmin.class, cardinality = ReferenceCardinality.MANDATORY_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
    private final Collection<RemoteServiceAdmin> mRemoteServiceAdmins;

    /** The exportable service tracker */
    private ServiceTracker<Object, Object> mTracker;
    /** Internal : export registrations */
    private final Map<ServiceReference<?>, Collection<ExportRegistration>> mExportRegistrations;
    /** The logger */
    private NemosLogger mLogger;

    /**
     * Constructor
     */
    public ExportTopologyManager() {
        super();

        mRemoteServiceAdmins = Collections
                .synchronizedCollection(new ArrayList<RemoteServiceAdmin>());

        mExportRegistrations = Collections
                .synchronizedMap(new HashMap<ServiceReference<?>, Collection<ExportRegistration>>());
    }

    /**
     * Export an eligible local service.
     * 
     * @param pReference
     *            The service reference
     */
    public void exportService(final ServiceReference<?> pReference) {

        synchronized (mExportRegistrations) {
            // Check service is not already exported
            final Collection<?> lRegistrations = mExportRegistrations.get(pReference);
            if (lRegistrations != null && !lRegistrations.isEmpty()) {
                return;
            }

            // Copy remote RSAs in order to minimize the synchronized block
            Collection<RemoteServiceAdmin> lRsaList;
            synchronized (mRemoteServiceAdmins) {
                lRsaList = new ArrayList<RemoteServiceAdmin>(mRemoteServiceAdmins);
            }

            // Try all RSAs until one exports successfully
            boolean lExportSuccessfull = false;
            final Iterator<RemoteServiceAdmin> lAdminsIt = lRsaList.iterator();
            while (lAdminsIt.hasNext() && !lExportSuccessfull) {

                lExportSuccessfull = export(pReference, lAdminsIt.next());
            }
        }
    }

    /**
     * Try to export a service with a specific RSA.
     * 
     * @param pReference
     *            The service reference
     * @param pAdmin
     *            The RSA to use
     * @return If export succeeded
     */
    private boolean export(final ServiceReference<?> pReference, final RemoteServiceAdmin pAdmin) {

        // Try export (copy the result collection for further
        // modifications)
        final Collection<ExportRegistration> lRegistrations = new ArrayList<ExportRegistration>(
                pAdmin.exportService(pReference, Collections.<String, Object> emptyMap()));

        // Filter error registrations
        for (final Iterator<ExportRegistration> lRegIt = lRegistrations.iterator(); lRegIt
                .hasNext();) {

            final ExportRegistration lRegistration = lRegIt.next();
            if (lRegistration.getException() != null) {
                final Object lId = pReference.getProperty(Constants.SERVICE_ID);
                final Object lPid = pReference.getProperty(Constants.SERVICE_PID);
                mLogger.error(TopologyMessage.EXPORT_FAILED, lRegistration.getException(), lId,
                        lPid);
                lRegIt.remove();
            }
        }

        // Register even empty registrations for later RSAs
        mExportRegistrations.put(pReference, lRegistrations);

        return !lRegistrations.isEmpty();
    }

    /**
     * Unexport a service which is no longer eligible to export.
     * 
     * @param pReference
     *            The service reference
     */
    public void unexportService(final ServiceReference<?> pReference) {
        final Collection<ExportRegistration> lRegistrations = mExportRegistrations
                .remove(pReference);
        if (lRegistrations != null) {
            for (final ExportRegistration lRegistration : lRegistrations) {
                lRegistration.close();
            }
        }
    }

    /**
     * Start the component.
     * 
     * @param pContext
     *            The bundle context
     * @throws InvalidSyntaxException
     *             If an init error occurs
     */
    @Activate
    public void start(final ComponentContext pContext) throws InvalidSyntaxException {
        mLogger = new NemosLogger(ExportTopologyManager.class);
        mLogger.setServiceReference(pContext.getServiceReference());

        mTracker = new ServiceTracker<Object, Object>(pContext.getBundleContext(),
                FilterBuilder.getFilter(Constants.SERVICE_EXPORTED_INTERFACES, "*"), null) { //$NON-NLS-1$

            /**
             * {@inheritDoc}
             * 
             * @see org.osgi.util.tracker.ServiceTracker#addingService(org.osgi.framework.ServiceReference)
             */
            @Override
            public Object addingService(final ServiceReference<Object> pReference) {
                final Object lService = super.addingService(pReference);
                exportService(pReference);
                return lService;
            }

            /**
             * {@inheritDoc}
             * 
             * @see org.osgi.util.tracker.ServiceTracker#removedService(org.osgi.framework.ServiceReference,
             *      java.lang.Object)
             */
            @Override
            public void removedService(final ServiceReference<Object> pReference,
                    final Object pService) {
                super.removedService(pReference, pService);

                unexportService(pReference);
            }
        };
        mTracker.open();
    }

    /**
     * Stop the component.
     */
    @Deactivate
    public void stop() {
        mTracker.close();
    }

    /**
     * Bind a new RSA service.
     * 
     * @param pRsa
     *            The new service
     */
    public void bindRsa(final RemoteServiceAdmin pRsa) {
        synchronized (mExportRegistrations) {

            // Try to export known endpoints
            // Local copy because of concurrent modifications
            final Collection<Entry<ServiceReference<?>, Collection<ExportRegistration>>> lEntries = new ArrayList<Entry<ServiceReference<?>, Collection<ExportRegistration>>>(
                    mExportRegistrations.entrySet());
            for (final Entry<ServiceReference<?>, Collection<ExportRegistration>> lEntry : lEntries) {
                if (lEntry.getValue().isEmpty()) {
                    export(lEntry.getKey(), pRsa);
                }
            }

            mRemoteServiceAdmins.add(pRsa);
        }
    }

    /**
     * Unbind a RSA service.
     * 
     * @param pRsa
     *            The service to unbind
     */
    public void unbindRsa(final RemoteServiceAdmin pRsa) {
        mRemoteServiceAdmins.remove(pRsa);
    }
}
